# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: inkyu

import datetime
import re
from collections import namedtuple
from datetime import timedelta
from enum import Enum

from coin.base.datetime_util import to_datetime
from coin.exchange.okex_futures.kr_rest.futures_product import OkexFuturesProduct

FillStat = namedtuple('FillStat', ['timestamp', 'side'])


class _OrderSide(Enum):
  UNKNOWN = 0
  BUY = 1
  SELL = 2


def side_from_sign(signed):
  if int(signed) == 1:
    return _OrderSide.BUY
  elif int(signed) == -1:
    return _OrderSide.SELL
  else:
    return _OrderSide.UNKNOWN


def datetime_diff_or_none(dt2, dt1):
  if dt2 is None or dt1 is None:
    return None
  return dt2 - dt1


def format_timedelta_long(time_delta):
  if time_delta is None:
    return ''

  ts = time_delta.total_seconds()
  h = int(ts / 3600)
  m = int(ts / 60) % 60
  s = int(ts % 60)
  return '%02d:%02d:%02d' % (h, m, s)


def format_timedelta_short(time_delta):
  if time_delta is None:
    return ''
  parsed = int(time_delta.total_seconds())
  return '%.2fH' % (parsed / (60.0 * 60.0))


def format_fill_stat(fill_stat, cur_time):
  if fill_stat is None:
    return ''

  timediff = cur_time - fill_stat.timestamp
  if fill_stat.side == _OrderSide.BUY:
    side_str = '+'
  elif fill_stat.side == _OrderSide.SELL:
    side_str = '-'
  else:
    side_str = ''
  return '%s%s' % (format_timedelta_long(timediff), side_str)


def format_time_marked(time_marked):
  if time_marked is None:
    return ''
  return datetime.datetime.strftime(time_marked, "%m-%d %H:%M")


def side_from_str(side_str):
  if side_str in ('BUY', 'OkexFuturesOrderSide.BUY_OPEN', 'OkexFuturesOrderSide.BUY_CLOSE'):
    return _OrderSide.BUY
  elif side_str in ('SELL', 'OkexFuturesOrderSide.SELL_OPEN', 'OkexFuturesOrderSide.SELL_CLOSE'):
    return _OrderSide.SELL
  else:
    return _OrderSide.UNKNOWN


def _extract_datetime_1(line):
  timestamp_format = '%Y-%m-%d %H:%M:%S,%f'  # Fucking european style
  splitted = line.split()
  try:
    dt = datetime.datetime.strptime('%s %s' % (splitted[1], splitted[2]), timestamp_format)
    return dt
  except (ValueError, IndexError):
    return None


def _extract_datetime_2(line):
  timestamp_format = 'I%m%d %H:%M:%S.%f'
  splitted = line.split()
  try:
    dt = (datetime.datetime.strptime('%s %s' % (splitted[0], splitted[1]),
                                     timestamp_format).replace(year=datetime.date.today().year))
    return dt
  except (ValueError, IndexError):
    return None


def floor_hour(dt):
  return datetime.datetime(dt.year, dt.month, dt.day, dt.hour)


def ceil_hour(dt_orig):
  dt = dt_orig + datetime.timedelta(hours=1) - datetime.timedelta(microseconds=1)
  return datetime.datetime(dt.year, dt.month, dt.day, dt.hour)


def floor_day(dt):
  return datetime.datetime(dt.year, dt.month, dt.day)


def extract_datetime(line):
  dt = _extract_datetime_1(line)
  if dt is not None:
    return dt
  return _extract_datetime_2(line)


class ParserState(Enum):
  READY = 0
  HAS_DATETIME = 1
  HAS_BALANCE = 2
  DONE = 3


class LogParser(object):
  def __init__(self,
               name,
               group,
               quote,
               balance_marker='BTC',
               position_marker='USD',
               fill_collector=None,
               flexibility=timedelta(minutes=10),
               adjust_withdrawal=True,
               use_position_1=False,
               spot_mode=False):
    self._last_fills = {}

    self._adjust_withdrawal = adjust_withdrawal
    self._adjust_nonunit = {'BTC': 2., 'JPY': 2000000, 'EOS': 1000, 'ETH': 40, 'USDT': 20000}
    self._adjust_unit = {'BTC': 1., 'JPY': 1000000., 'EOS': 500, 'ETH': 10, 'USDT': 1000}
    self._adjust_tolerance = {'BTC': 0.05, 'JPY': 50000., 'EOS': 1, 'ETH': 0.1, 'USDT': 20}
    self._value_offset = 0.

    self._name = name
    self._group = group
    self._quote = quote
    self._time = None
    self._time_marked = None
    self._state = ParserState.READY
    self._fill_collector = fill_collector
    self._use_position_1 = use_position_1
    self._balance_marker = balance_marker
    self._position_marker = position_marker

    self._position_last = None
    self._position_last_1 = None
    self._position_last_2 = None

    self._value_last = None
    self._value_last_1 = None
    self._value_last_2 = None
    self._values = {}
    self._turnovers = {}

    self._processing_time = None
    self._spot_mode = spot_mode

  def handle_price(self, line: str):
    if line.startswith('*'):
      splitted = line.split()
      if not self._spot_mode:
        if len(splitted) == 5 and splitted[1] == self._balance_marker:
          position_1 = None
          if self._use_position_1:
            position_1 = float(splitted[3])
          return [float(splitted[2]), position_1, float(splitted[4])]
        elif len(splitted) == 3 and splitted[1] == self._balance_marker:
          return [float(splitted[2]), None, float(splitted[2])]
        elif len(splitted) == 4 and splitted[1] == self._balance_marker:
          return [float(splitted[2]), None, float(splitted[2])]
      else:
        if len(splitted) == 4 and splitted[1] == self._balance_marker:
          return [float(splitted[3]), None, float(splitted[3])]

  def handle_position(self, line: str):
    if line.startswith('*'):
      splitted = line.split()

      if isinstance(self._position_marker, OkexFuturesProduct):
        pr = self._position_marker
        position_marker = OkexFuturesProduct(pr.base,
                                             pr.quote,
                                             pr._contract_type,
                                             current_datetime=self._time).full_symbol
      else:
        position_marker = self._position_marker

      if len(splitted) == 5 and splitted[1] == position_marker:
        return [float(splitted[2]), float(splitted[3]), float(splitted[4])]
      elif len(splitted) == 4 and splitted[1] == position_marker:
        return [float(splitted[2]), 0., float(splitted[2])]
      elif len(splitted) == 7 and splitted[1] == position_marker:
        pnl_pos = float(splitted[3]) - float(splitted[2])
        return [pnl_pos, 0., pnl_pos]

  def detect_datetime(self, line: str):
    matches = re.search('^\* (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}.\d{6})$', line)
    if matches is None:
      return None
    marker = datetime.datetime.strptime("%s %s" % (matches.group(1), matches.group(2)),
                                        "%Y-%m-%d %H:%M:%S.%f")
    return marker

  def update_values(self, value, value_1, value_2):
    if 0 in (value, value_1, value_2):
      return

    if (self._adjust_withdrawal and self._quote in ('BTC', 'JPY', 'ETH', 'EOS', 'USDT')
        and self._value_last is not None):
      adj_nonunit = self._adjust_nonunit[self._quote]
      adj_unit = self._adjust_unit[self._quote]
      adj_tor = self._adjust_tolerance[self._quote]
      adj_value = value + self._value_offset
      diff = adj_value - self._value_last
      if abs(diff) > adj_unit - adj_tor:
        for i in range(1, 20):
          adj_amt = i * adj_unit
          if (adj_amt - adj_tor) < diff < (adj_amt + adj_tor):
            self._value_offset -= adj_amt
            break
          elif -(adj_amt + adj_tor) < diff < -(adj_amt - adj_tor):
            self._value_offset += adj_amt
            break
        if self._value_offset == 0 and abs(diff) > adj_nonunit:
          self._value_offset -= diff
      value = value + self._value_offset

    self._value_last = value
    self._value_last_1 = value_1
    self._value_last_2 = value_2
    key = floor_hour(self._time)
    if key not in self._values:
      self._values[key] = value
    key = ceil_hour(self._time)
    self._values[key] = value
    self._garbage_collect(self._time)

  def _garbage_collect(self, time):
    target_dt = floor_hour(time - datetime.timedelta(hours=24))
    self._values = {key: value for key, value in self._values.items() if key >= target_dt}
    self._turnovers = {key: value for key, value in self._turnovers.items() if key >= target_dt}

  def update_positions(self, position, position_1, position_2):
    self._position_last = position
    self._position_last_1 = position_1
    self._position_last_2 = position_2

  def add_turnover(self, time, turnover):
    for key in self._turnovers.keys():
      self._turnovers[key] += turnover
    key = floor_hour(time)
    if key not in self._turnovers:
      self._turnovers[key] = turnover
    self._garbage_collect(time)

  def handle_fill_dump(self, line: str):
    comma_separated = line.split(',')
    if len(comma_separated) == 13 and comma_separated[0] == '"FILL_DUMP"':
      timestamp = to_datetime(int(comma_separated[1]))
      exchange_id = int(comma_separated[3])
      side = side_from_sign(comma_separated[4])
      # _fill_price = float(comma_separated[5])
      fill_qty = float(comma_separated[6])
      self._last_fills[exchange_id] = FillStat(timestamp, side)
      self.add_turnover(timestamp, abs(fill_qty))
      return True

    elif len(comma_separated) == 9 and comma_separated[0] == '"FILL_DUMP"':
      timestamp = to_datetime(int(comma_separated[1]))
      exchange_id = 2
      side = side_from_sign(comma_separated[3])
      # _fill_price = float(comma_separated[4])
      fill_qty = float(comma_separated[5]) * 10
      self._last_fills[exchange_id] = FillStat(timestamp, side)
      self.add_turnover(timestamp, abs(fill_qty))
      return True

    elif len(comma_separated) == 12 and comma_separated[0] == '"FILL_DUMP"':
      # Bitfinex: we must consider this
      timestamp = to_datetime(int(comma_separated[1]))
      exchange_id = 2
      exchange = str(comma_separated[3])
      symbol = str(comma_separated[4])
      side = side_from_sign(comma_separated[5])
      fill_price = float(comma_separated[6])
      fill_qty = float(comma_separated[7])
      self._last_fills[exchange_id] = FillStat(timestamp, side)
      if exchange == '"OkexFutures"':
        if 'BTC-USD' in symbol:
          self.add_turnover(timestamp, abs(fill_qty * 100))
        else:
          self.add_turnover(timestamp, abs(fill_qty * 10))
      elif exchange == '"Bitmex"':
        self.add_turnover(timestamp, abs(fill_qty))
      else:
        self.add_turnover(timestamp, abs(fill_qty * fill_price))
      return True

    return False

  def handle_fill_collector(self, line: str):
    timestamp_format = '%Y-%m-%d %H:%M:%S,%f'  # Fucking european style
    side = None
    result = None
    splitted = line.split()
    fill_qty = None
    timestamp = None
    exchange_id = None

    # DEBUG 2018-03-16 17:35:21,280 Bitmex] FILL XBTUSD SELL 35000.000000@8552.0
    if (len(splitted) == 8 and splitted[0] in ('DEBUG', 'INFO') and splitted[3] == 'Bitmex]'
        and splitted[4] == 'FILL'):
      exchange_id = 2
      timestamp = datetime.datetime.strptime("%s %s" % (splitted[1], splitted[2]), timestamp_format)
      side = side_from_str(splitted[6])
      fill_qty_s, fill_price_s = splitted[7].split('@')
      fill_qty = float(fill_qty_s)
      _fill_price = float(fill_price_s)
      result = True

    # INFO 2018-02-26 10:43:40,355 SoftArbStrategy] [1] FILL OkexFuturesOrderSide.BUY_OPEN
    #   1.000000 @ 9549.8500
    elif (len(splitted) == 10 and splitted[0] == 'INFO' and splitted[3] == 'SoftArbStrategy]'
          and splitted[8] == '@'):
      exchange_id = 1
      timestamp = datetime.datetime.strptime("%s %s" % (splitted[1], splitted[2]), timestamp_format)
      side = side_from_str(splitted[6])
      fill_qty = float(splitted[7]) * 100
      _fill_price = float(splitted[9])
      result = True

    # INFO 2018-06-13 18:30:10,472 Bitflyer order_gateway.py:275] FILL FX_BTC_JPY SELL
    #   1.000000 @ 721370.000000
    # I0725 08:11:47.967512 139756067235648 order_gateway.py:287] FILL FX_BTC_JPY  BUY
    #   1.400000 @ 965684.000000
    elif (len(splitted) >= 6 and 'FILL' in splitted and 'FX_BTC_JPY' in splitted):
      base_idx = splitted.index('FILL')
      exchange_id = 2
      timestamp = extract_datetime(line)
      side = side_from_str(splitted[base_idx + 2])
      _fill_price = float(splitted[base_idx + 5])
      fill_qty = float(splitted[base_idx + 3]) * _fill_price
      result = True

    if result:
      self._last_fills[exchange_id] = FillStat(timestamp, side)
      self.add_turnover(timestamp, fill_qty)
    return result

  def parse_log_line(self, line: str):
    if self._fill_collector == 'fill_collector':  # Parse the log directly
      if self.handle_fill_collector(line):
        return
    elif self._fill_collector == 'csv':
      if self.handle_fill_dump(line):
        return

    if self._state == ParserState.READY:
      datetime_parsed = self.detect_datetime(line)
      if datetime_parsed is not None:
        self._state = ParserState.HAS_DATETIME
      self._time = datetime_parsed
    elif self._state == ParserState.HAS_DATETIME:
      values = self.handle_price(line)
      if values is not None:
        value, value_1, value_2 = values
        self._state = ParserState.HAS_BALANCE
        self.update_values(value, value_1, value_2)
    elif self._state == ParserState.HAS_BALANCE:
      positions = self.handle_position(line)
      if positions is not None:
        position, position_1, position_2 = positions
        self._state = ParserState.READY
        self.update_positions(position, position_1, position_2)

  def _get_turnover(self, dt):
    keys = [key for key in self._turnovers.keys() if key > dt]
    if len(keys) > 0:
      return self._turnovers[min(keys)]
    else:
      return None

  def _get_value(self, dt):
    keys = [key for key in self._values.keys() if key > dt]
    if len(keys) > 0:
      return self._values[min(keys)]
    else:
      return None

  def get_result(self, *, time_current):
    time_current = time_current

    time_marked = floor_day(time_current)
    mark_time = '%s (%s)' % (format_time_marked(time_marked),
                             format_timedelta_short(datetime_diff_or_none(
                                 time_current, time_marked)))
    return {
        'name':
            self._name,
        'group':
            self._group,
        'quote':
            self._quote,
        'account_value':
            self._value_last,
        'mark_value':
            0,  # self._value_mark,
        'pnl_mark':
            datetime_diff_or_none(self._value_last, self._get_value(floor_day(time_current))),
        'pnl_4h':
            datetime_diff_or_none(
                self._value_last,
                self._get_value(floor_hour(time_current) - datetime.timedelta(hours=4))),
        'pnl_24h':
            datetime_diff_or_none(
                self._value_last,
                self._get_value(floor_hour(time_current) - datetime.timedelta(hours=24))),
        'turnover_mark':
            self._get_turnover(floor_day(time_current)),
        'turnover_4h':
            self._get_turnover(floor_hour(time_current) - datetime.timedelta(hours=4)),
        'turnover_24h':
            self._get_turnover(floor_hour(time_current) - datetime.timedelta(hours=24)),
        'balance_1':
            self._value_last_1 or None,
        'balance_2':
            self._value_last_2 or None,
        'position_1':
            self._position_last_1 or None,
        'position_2':
            self._position_last_2 or None,
        'fill_1':
            format_fill_stat(self._last_fills.get(1), time_current),
        'fill_2':
            format_fill_stat(self._last_fills.get(2), time_current),
        'mark_time':
            mark_time,
        'processing_time':
            self._processing_time,
        'last_log':
            format_timedelta_long(datetime_diff_or_none(time_current, self._time))
    }
